package qrcode;

import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;

public class QRCode {

  public static final class Point {

    private int x;

    private int y;

    public Point(int x, int y) {
      this.x = x;
      this.y = y;
    }

    public int getX() {
      return x;
    }

    public int getY() {
      return y;
    }
  }

  public static final int FINDER_PATTERN_SIZE = 7;

  public static boolean isAlignmentPatternBlack(final int x, final int y) {
    if ((x == 1 || x == 3) && (y >= 1 && y <= 3))
      return false;
    if ((y == 1 || y == 3) && (x >= 1 && x <= 3))
      return false;
    return true;
  }

  public static boolean isBlackInFinderPattern(int x, int y) {
    if ((x == 1 || x == 5) && (y >= 1 && y <= 5))
      return false;
    if ((y == 1 || y == 5) && (x >= 1 && x <= 5))
      return false;
    return true;
  }

  public static void main(String[] args) {
    QRCode qr = new QRCode(Version.VERSION_1, ErrorCorrectionLevel.L, DataMaskPattern.NO_MASK);
    qr.printToStream(System.out, true);
    System.out.println(qr.version.getNumberOfDataModules());
    
    Boolean[] test = new Boolean[qr.version.getNumberOfDataModules()-10];
    Arrays.fill(test, Boolean.TRUE);
    
    qr.setDataBits(Arrays.asList(test).iterator());
    qr.printToStream(System.out, true);
  }

  private boolean black[][];

  private ErrorCorrectionLevel errorCorrectionLevel;

  private DataMaskPattern mask;

  private Version version;

  public QRCode(Version version, ErrorCorrectionLevel errorCorrectionLevel, DataMaskPattern mask) {
    this.version = version;
    this.mask = mask;
    this.errorCorrectionLevel = errorCorrectionLevel;
    black = new boolean[version.getNumberOfModulesPerSide()][version.getNumberOfModulesPerSide()];
    paintFinderPattern();
    paintAlignmentPattern();
    paintAlwaysBlackModule();
    paintTimingPattern();
    paintVersionInformation();
    paintFormatInformation();
  }

  private void checkReady(int x, int y) {
    // TODO ggf. Exception werfen, wenn bit Strom zu kurz.
  }

  public Point getAlignmentPatternCenter(final int x, final int y) {
    final int[] alignmentPatternCenters = version.getAlignmentPatternCenters();
    if (alignmentPatternCenters == null || alignmentPatternCenters.length == 0)
      return null;
    if (isFinderPattern(x, y))
      return null;

    int xCenter = -1;
    int yCenter = -1;
    for (int center : alignmentPatternCenters) {
      if (x >= center - 2 && x <= center + 2)
        xCenter = center;
      if (y >= center - 2 && y <= center + 2)
        yCenter = center;
    }
    if (xCenter == -1 || yCenter == -1 || isFinderPattern(xCenter, yCenter))
      return null;
    return new Point(xCenter, yCenter);
  }

  public BitHelper getFormatInfoBits() {
    return FormatInfoHelper.getFormatInfoBitsAfterMaskingBits(errorCorrectionLevel, mask);
  }

  public int getHeight() {
    return version.getNumberOfModulesPerSide();
  }

  public int getVersionBottomLeftStartY() {
    return getHeight() - 4 - FINDER_PATTERN_SIZE;
  }

  public int getVersionTopRightStartX() {
    return getWidth() - 4 - FINDER_PATTERN_SIZE;
  }

  public int getWidth() {
    return version.getNumberOfModulesPerSide();
  }

  public boolean hasVersionInformation() {
    return version.hasVersionInformation();
  }

  public boolean isAlignmentPattern(final int x, final int y) {
    return getAlignmentPatternCenter(x, y) != null;
  }

  public boolean isAlwaysDarkModule(final int x, final int y) {
    return x == FINDER_PATTERN_SIZE + 1 && y == getHeight() - FINDER_PATTERN_SIZE - 1;
  }

  public boolean isBlack(final int x, final int y) {
    return black[y][x];
  }

  public boolean isDataBit(final int x, final int y) {
    return willBeMasked(x, y);
  }

  public boolean isFinderPattern(int x, int y) {
    if (x < FINDER_PATTERN_SIZE && y < FINDER_PATTERN_SIZE)
      return true;
    if (x < FINDER_PATTERN_SIZE && y >= getHeight() - FINDER_PATTERN_SIZE)
      return true;
    if (y < FINDER_PATTERN_SIZE && x >= getWidth() - FINDER_PATTERN_SIZE)
      return true;
    return false;
  }

  public boolean isFinderPatternSeparator(final int x, final int y) {
    if (x == FINDER_PATTERN_SIZE && (y <= FINDER_PATTERN_SIZE || y >= getHeight() - FINDER_PATTERN_SIZE - 1))
      return true;
    if (y == FINDER_PATTERN_SIZE && (x <= FINDER_PATTERN_SIZE || x >= getWidth() - FINDER_PATTERN_SIZE - 1))
      return true;
    if (x == getWidth() - FINDER_PATTERN_SIZE - 1 && y <= FINDER_PATTERN_SIZE)
      return true;
    if (y == getHeight() - FINDER_PATTERN_SIZE - 1 && x <= FINDER_PATTERN_SIZE)
      return true;
    return false;
  }

  public boolean isFormatInformation(int x, int y) {
    if (x == FINDER_PATTERN_SIZE + 1
        && (y < FINDER_PATTERN_SIZE - 1 || y == FINDER_PATTERN_SIZE || y == FINDER_PATTERN_SIZE + 1 || y >= getHeight()
            - FINDER_PATTERN_SIZE)) {
      return true;
    }
    if (y == FINDER_PATTERN_SIZE + 1
        && (x < FINDER_PATTERN_SIZE - 1 || x == FINDER_PATTERN_SIZE || x >= getWidth() - FINDER_PATTERN_SIZE - 1)) {
      return true;
    }
    return false;
  }

  public boolean isTimingPattern(int x, int y) {
    if (x == FINDER_PATTERN_SIZE - 1 && y > FINDER_PATTERN_SIZE && y < getHeight() - FINDER_PATTERN_SIZE - 1)
      return !isAlignmentPattern(x, y);
    if (y == FINDER_PATTERN_SIZE - 1 && x > FINDER_PATTERN_SIZE && x < getWidth() - FINDER_PATTERN_SIZE - 1)
      return !isAlignmentPattern(x, y);
    return false;
  }

  public boolean isTimingPatternBlack(int x, int y) {
    return (x + y) % 2 == 0;
  }

  public boolean isVersionInformation(final int x, final int y) {
    if (!hasVersionInformation())
      return false;
    if (x < 6 && y >= getHeight() - 11 && y <= getHeight() - 9)
      return true;
    if (y < 6 && x >= getWidth() - 11 && x <= getWidth() - 9)
      return true;

    return false;
  }

  public final boolean isWhite(final int x, final int y) {
    return !isBlack(x, y);
  }

  private void paintAlignmentPattern() {
    int[] alignmentPatternCenters = version.getAlignmentPatternCenters();
    if (alignmentPatternCenters == null || alignmentPatternCenters.length == 0)
      return;
    for (int i = 0; i < alignmentPatternCenters.length; i++) {
      int x = alignmentPatternCenters[i];
      for (int j = 0; j < alignmentPatternCenters.length; j++) {
        int y = alignmentPatternCenters[j];
        if (!isFinderPattern(x, y)) {
          paintAlignmentPattern(x, y);
        }
      }
    }
  }

  private void paintAlignmentPattern(final int cx, final int cy) {
    setModule(cx, cy, true);
    for (int i = -2; i <= 2; i++) {
      setModule(cx + i, cy - 2, true);
      setModule(cx + i, cy + 2, true);
      setModule(cx - 2, cy + i, true);
      setModule(cx + 2, cy + i, true);
    }
  }

  private void paintAlwaysBlackModule() {
    setModule(8, 4 * version.getVersionNumber().intValue() + 9, true);
  }

  private void paintFinderPattern() {
    paintFinderPattern(0, 0);
    paintFinderPattern(0, getWidth() - FINDER_PATTERN_SIZE);
    paintFinderPattern(getHeight() - FINDER_PATTERN_SIZE, 0);
  }

  private void paintFinderPattern(int dx, int dy) {
    for (int i = 0; i < FINDER_PATTERN_SIZE; i++) {
      setModule(dx + i, dy, true);
      setModule(dx, dy + i, true);
      setModule(dx + i, dy + FINDER_PATTERN_SIZE - 1, true);
      setModule(dx + FINDER_PATTERN_SIZE - 1, dy + i, true);
    }
    for (int i = 2; i <= 4; i++)
      for (int j = 2; j <= 4; j++)
        setModule(dx + i, dy + j, true);
  }

  private void paintFormatInformation() {
    BitHelper formatInfoBits = FormatInfoHelper.getFormatInfoBitsAfterMaskingBits(errorCorrectionLevel, mask);
    int x = getWidth() - 1;
    int y = 0;
    for (int i = 0; i < formatInfoBits.getSize(); i++) {
      if (i == FINDER_PATTERN_SIZE - 1) {
        y = FINDER_PATTERN_SIZE;
      }
      if (i == FINDER_PATTERN_SIZE + 1) {
        y = getHeight() - FINDER_PATTERN_SIZE;
        x = FINDER_PATTERN_SIZE;
      }
      if (i == FINDER_PATTERN_SIZE + 2) {
        x = FINDER_PATTERN_SIZE - 2;
      }
      boolean bit = formatInfoBits.isBitSet(i);
      if (bit) {
        setModule(FINDER_PATTERN_SIZE + 1, y, true);
        setModule(x, FINDER_PATTERN_SIZE + 1, true);
      }
      x--;
      y++;
    }
  }

  private void paintTimingPattern() {
    for (int i = FINDER_PATTERN_SIZE + 1; i < getWidth() - FINDER_PATTERN_SIZE - 1; i += 2) {
      setModule(i, FINDER_PATTERN_SIZE - 1, true);
      setModule(FINDER_PATTERN_SIZE - 1, i, true);
    }
  }

  private void paintVersionInformation() {
    if (!hasVersionInformation()) {
      return;
    }
    BitHelper bits = new BitHelper(version.getVersionInformationBits(), 18);
    for (int sx = 0; sx < 5; sx++) {
      for (int sy = 0; sy < 3; sy++) {
        int idx = sy + 3 * sx;
        if (bits.isBitSet(idx))
          setModule(sx, sy + getVersionBottomLeftStartY(), true);
      }
    }
    for (int sx = 0; sx < 3; sx++) {
      for (int sy = 0; sy < 5; sy++) {
        int idx = sx + 3 * sy;
        if (bits.isBitSet(idx))
          setModule(sx + getVersionTopRightStartX(), sy, true);
      }
    }
  }

  public void printToStream(PrintStream stream, boolean printIndex) {
    if (printIndex) {
      stream.print(" ");
      for (int x = 0; x < getWidth(); x++) {
        stream.print(x % 10);
      }
      stream.println();
    }
    for (int y = 0; y < getHeight(); y++) {
      if (printIndex)
        stream.print(y % 10);
      for (int x = 0; x < getWidth(); x++) {
        stream.print(isBlack(x, y) ? 'X' : '.');
      }
      stream.println();
    }
  }

  public void setDataBits(Iterator<Boolean> bits) {
    int nrOfColumns = (getWidth() - 1) / 2;
    boolean upwards = true;
    for (int col = nrOfColumns-1; col >= 0; col--) {
      int colStartX = col * 2 + (col < 3 ? 0 : 1);
      if (upwards) {
        // upwards
        for (int y = getHeight() - 1; y >= 0; y--) {
          // right bit
          if (isDataBit(colStartX + 1, y)) {
            setModule(colStartX + 1, y, bits.next().booleanValue());
            if (!bits.hasNext()) {
              checkReady(colStartX + 1, y);
              return;
            }
          }
          // left bit
          if (isDataBit(colStartX, y)) {
            setModule(colStartX, y, bits.next().booleanValue());
            if (!bits.hasNext()) {
              checkReady(colStartX, y);
              return;
            }
          }
        }
        upwards = false;
      } else {
        // downwards
        for (int y = 0; y < getHeight(); y++) {
          if (isDataBit(colStartX + 1, y)) {
            setModule(colStartX + 1, y, bits.next().booleanValue());
            if (!bits.hasNext()) {
              checkReady(colStartX + 1, y);
              return;
            }
          }
          // left bit
          if (isDataBit(colStartX, y)) {
            setModule(colStartX, y, bits.next().booleanValue());
            if (!bits.hasNext()) {
              checkReady(colStartX, y);
              return;
            }
          }
        }
        upwards = true;
      }
    }
  }

  public void setMask(DataMaskPattern newMask) {
    DataMaskPattern oldMask = this.mask;
    if (oldMask == newMask)
      return;
    for (int y = 0; y < getHeight(); y++) {
      for (int x = 0; x < getWidth(); x++) {
        if (willBeMasked(x, y)) {
          boolean value = oldMask.mask(x, y, black[y][x]);
          this.black[y][x] = newMask.mask(x, y, value);
        }
      }
    }
    this.mask = newMask;
    paintFormatInformation();
  }

  private void setModule(int x, int y, boolean black) {
    boolean masked;
    if (willBeMasked(x, y)) {
      masked = this.mask.mask(x, y, black);
    } else {
      masked = black;
    }
    this.black[y][x] = masked;
  }

  public boolean willBeMasked(final int x, final int y) {
    if (isFinderPatternSeparator(x, y))
      return false;
    if (isFinderPattern(x, y))
      return false;
    if (isAlignmentPattern(x, y))
      return false;
    if (isVersionInformation(x, y))
      return false;
    if (isAlwaysDarkModule(x, y))
      return false;
    if (isTimingPattern(x, y))
      return false;
    if (isFormatInformation(x, y))
      return false;
    return true;
  }

  public Version getVersion() {
    return version;
  }
}